package com.auntvt.timo.jpax.core.quick;

import com.auntvt.timo.jpax.core.quick.interfaces.QuickSpecification;
import com.auntvt.timo.jpax.core.quick.interfaces.SpecificationContainer;
import com.auntvt.timo.jpax.core.quick.node.CriteriaNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * spring data jpa Quick操作抽象类，用于构建Jpa Specification对象
 *
 * @author 小懒虫 <auntvt@163.com>
 * @date 2021/7/31
 */
@Slf4j
public abstract class AbstractJpaQuick<T, Children, R> extends AbstractQuick<Children, SpecificationContainer<T>, R> implements QuickSpecification<T> {

    /**
     * 联表字段名分割符号
     */
    private static final String JOIN_FIELD_NAME_SPLIT = ".";

    /**
     * 获取字段Expression对象
     *
     * @param node CriteriaNode
     * @param root From
     * @return Expression对象
     */
    private static <E, Y> Expression<Y> getFieldExp(CriteriaNode node, From<E, Y> root) {
        // 判断字段名是否为空
        if (node.getFieldName() == null) {
            return null;
        }

        Expression<Y> expression;
        // 判断是否联表查询
        String[] fieldNames = StringUtils.split(node.getFieldName(), JOIN_FIELD_NAME_SPLIT);
        if (fieldNames != null && fieldNames.length > 0) {
            expression = root.get(fieldNames[0]);
            for (int i = 1; i < fieldNames.length; i++) {
                expression = ((Path<Y>) expression).get(fieldNames[i]);
            }
        } else {
            expression = root.get(node.getFieldName());
        }

        // 判断是否进行类型转换
        if (node.getAsFieldType() != null) {
            @SuppressWarnings("unchecked")
            Expression<?> as = expression = (Expression<Y>) expression.as(node.getAsFieldType());
        }
        return expression;
    }

    /**
     * 获取字段Expression对象，消除From泛型，使其能融入到需要泛型一致的参数中
     *
     * @param node CriteriaNode
     * @param root From
     * @return Expression对象
     */
    @SuppressWarnings("unchecked")
    private static <Y> Expression<Y> getFieldExpInto(CriteriaNode node, From root) {
        return getFieldExp(node, root);
    }

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        List<Predicate> predicates = getPredicates(criteriaNodes, root, cb);
        if (quickContainer != null) {
            quickContainer.callback(root, query, cb, predicates);
        }
        return query.where(predicates.toArray(new Predicate[0])).getRestriction();
    }

    /**
     * 获取Predicate数组
     *
     * @param criteriaNodes 条件节点列表
     * @param root          Root
     * @param cb            CriteriaBuilder
     */
    private <E, Y> List<Predicate> getPredicates(List<CriteriaNode> criteriaNodes, From<E, Y> root, CriteriaBuilder cb) {
        List<Predicate> predicates = new ArrayList<>();
        for (CriteriaNode node : criteriaNodes) {
            switch (node.getType()) {
                case EQ:
                    predicates.add(cb.equal(getFieldExp(node, root), node.getValue()));
                    break;
                case NE:
                    predicates.add(cb.notEqual(getFieldExp(node, root), node.getValue()));
                    break;
                case GT:
                    if (node.getValue() instanceof Number) {
                        predicates.add(cb.gt(getFieldExpInto(node, root), (Number) node.getValue()));
                    } else {
                        predicates.add(cb.greaterThan(getFieldExpInto(node, root), (Comparable) node.getValue()));
                    }
                    break;
                case GE:
                    if (node.getValue() instanceof Number) {
                        predicates.add(cb.ge(getFieldExpInto(node, root), (Number) node.getValue()));
                    } else {
                        predicates.add(cb.greaterThanOrEqualTo(getFieldExpInto(node, root), (Comparable) node.getValue()));
                    }
                    break;
                case LT:
                    if (node.getValue() instanceof Number) {
                        predicates.add(cb.lt(getFieldExpInto(node, root), (Number) node.getValue()));
                    } else {
                        predicates.add(cb.lessThan(getFieldExpInto(node, root), (Comparable) node.getValue()));
                    }
                    break;
                case LE:
                    if (node.getValue() instanceof Number) {
                        predicates.add(cb.le(getFieldExpInto(node, root), (Number) node.getValue()));
                    } else {
                        predicates.add(cb.lessThanOrEqualTo(getFieldExpInto(node, root), (Comparable) node.getValue()));
                    }
                    break;
                case LIKE:
                    predicates.add(cb.like(getFieldExpInto(node, root), String.valueOf(node.getValue())));
                    break;
                case LIKE_BOTH:
                    predicates.add(cb.like(getFieldExpInto(node, root), "%" + node.getValue() + "%"));
                    break;
                case LIKE_LEFT:
                    predicates.add(cb.like(getFieldExpInto(node, root), "%" + node.getValue()));
                    break;
                case LIKE_RIGHT:
                    predicates.add(cb.like(getFieldExpInto(node, root), node.getValue() + "%"));
                    break;
                case NOT_LIKE:
                    predicates.add(cb.notLike(getFieldExpInto(node, root), String.valueOf(node.getValue())));
                    break;
                case BETWEEN:
                    Object[] values = (Object[]) node.getValue();
                    predicates.add(cb.between(getFieldExpInto(node, root), (Comparable) values[0], (Comparable) values[1]));
                    break;
                case NOT_BETWEEN:
                    Object[] notValues = (Object[]) node.getValue();
                    predicates.add(cb.not(cb.between(getFieldExpInto(node, root), (Comparable) notValues[0], (Comparable) notValues[1])));
                    break;
                case IS_NULL:
                    predicates.add(cb.isNull(getFieldExp(node, root)));
                    break;
                case IS_NOT_NULL:
                    predicates.add(cb.isNotNull(getFieldExp(node, root)));
                    break;
                case IN:
                    CriteriaBuilder.In<Object> in = cb.in(getFieldExp(node, root));
                    for (Object value : (Object[]) node.getValue()) {
                        in.value(value);
                    }
                    predicates.add(in);
                    break;
                case NOT_IN:
                    CriteriaBuilder.In<Object> notIn = cb.in(getFieldExp(node, root));
                    for (Object value : (Object[]) node.getValue()) {
                        notIn.value(value);
                    }
                    predicates.add(cb.not(notIn));
                    break;

                case AND:
                    predicates.add(cb.and(getArrayPredicates(node.getChildren(), root, cb)));
                    break;
                case OR:
                    predicates.add(cb.or(getArrayPredicates(node.getChildren(), root, cb)));
                    break;
                case NOT:
                    Predicate[] cp = getArrayPredicates(node.getChildren(), root, cb);
                    for (Predicate predicate : cp) {
                        predicates.add(cb.not(predicate));
                    }
                    break;

                case JOIN_INNER:
                    predicates.addAll(Arrays.asList(getArrayPredicates(node.getChildren(), root.join(node.getFieldName(), JoinType.INNER), cb)));
                    break;
                case JOIN_LEFT:
                    predicates.addAll(Arrays.asList(getArrayPredicates(node.getChildren(), root.join(node.getFieldName(), JoinType.LEFT), cb)));
                    break;
                case JOIN_RIGHT:
                    predicates.addAll(Arrays.asList(getArrayPredicates(node.getChildren(), root.join(node.getFieldName(), JoinType.RIGHT), cb)));
                    break;
                default:
                    log.warn("{}条件类型不存在", node.getType().name());
            }
        }

        return predicates;
    }

    /**
     * 获取Predicate数组对象
     */
    private <E, Y> Predicate[] getArrayPredicates(List<CriteriaNode> criteriaNodes, From<E, Y> root, CriteriaBuilder cb) {
        return getPredicates(criteriaNodes, root, cb).toArray(new Predicate[0]);
    }
}
